/**
 * Filter
 * Censors *** words.
 *
 * Cassie Chin, Stephen Smetana, Logan Widick
 * CSCE 3530 Program 2
 */

#define _GNU_SOURCE // get rid of the compile warning when using strcasestr

#include <string.h>
#include <stdlib.h> 
#include <stdbool.h>
#include <ctype.h>
#include "filter.h"

// the # of chars to check to see if the file identifies a text file
#define NUM_CHARS_TO_CHECK 100
#define FILTER_BUFFER_SIZE 1024

/**
 * \struct Word
 * A Word is an item in a list of words that will be filtered.
 */
typedef struct Word {
  /** the source word */
  char *uncensored;
  struct Word *next;
} Word;

/**
 * \struct Filter
 * A Filter contains a list of words to be filtered.
 */
typedef struct Filter {
  Word *head;
  int size;
} Filter;

/**
 * Constructor for a new word 
 * 
 * @param uncensored The uncensored word.
 */
Word *new_Word (char *uncensored) {
  if(!uncensored) return NULL;

  Word *new = (Word *) calloc (1, sizeof(Word));
  new->uncensored = strdup(uncensored);

  return new;
}

/**
 * Inserts a word into the filter
 * 
 * @param filter The filter to insert into
 * @param uncensored The word to insert into the filter.
 */
void Filter_insert (Filter *filter, char *uncensored) {
  Word *new = new_Word (uncensored);

  // Insert the head into the filter
  if (!filter->head) {
    filter->head = new;
    filter->size++;
  }
  // Search through the filter and insert it alphabetically
  else {
    Word *ptr;
    for (ptr = filter->head; ptr; ptr = ptr->next) {
      if (ptr->next == NULL) {
	ptr->next = new;
	filter->size++;
	return;
      }
      else if (strcasecmp (uncensored, ptr->next->uncensored) < 0) {
	new->next = ptr->next;
	ptr->next = new;
	filter->size++;
	break;
      }
    }
  }
}

/**
 * Constructor for the filter.
 *
 * @param fp The file containing the words to put into the filter.
 */
Filter *new_Filter(FILE *fp) {
  Filter *filter = (Filter *) calloc (1, sizeof(Filter));
  
  if (!filter) return NULL;
  if (!fp) return NULL;
  
 char buffer[FILTER_BUFFER_SIZE+2];
  int i = 0;
  while (fgets (buffer, FILTER_BUFFER_SIZE,fp)) {
    //remove trailing newline
    i = strlen(buffer) - 1;
    while(buffer[i] == '\r' || buffer[i] == '\n'){
	buffer[i] = '\0';
	i--;
    }

    //add to filter.
    Filter_insert (filter, buffer);
  }
  
  return filter;
}

/**
 * Filter destructor.
 * @param filter The filter to delete.
 */
void delete_Filter (Filter *filter) {
  if(!filter) return;
  Word *ptr = NULL;
  Word *ptr_next = NULL;
  
  ptr = filter->head;
  while(ptr)
  {
	ptr_next = ptr->next;
	free(ptr->uncensored);
	free(ptr);
	ptr = ptr_next;
   }
  free (filter);
}

/**
 * Determines whether a file is a text file. If everything in the file is a character or a new line then this is a file.
 * 
 * @param filter The filter to use
 * @param theFile Check to see if this is a text file or not.
 */
bool Filter_isTextFile(Filter *filter, FILE *theFile) {
	if(!filter || !theFile) return false;
	rewind(theFile);
	
	int i = 0;
	int theChar = 0;	
	for(i=0; i<NUM_CHARS_TO_CHECK; i++)
	{
		theChar = fgetc(theFile);
		//we reach eof: can vouch that the file is text.
		if(theChar == EOF) return true;
		// new line is not a character yet it is a valid symbol so continue
		else if (theChar == '\n'|| theChar == '\r') continue;
		//we reach a non-printable; exit.
		else if(!isprint(theChar)) return false;
	}
	return true;
}

/**
 * Filters a buffer by replacing all the bad words with stars.
 *
 * @param filter The filter to use.
 * @param rawText The buffer to filter.
 */
void Filter_filterLine(Filter *filter, char *rawText) {
	if(!filter || !rawText) return;

	char *match = NULL; // The bad word that was found in the buffer
	char *uncensored = NULL; // The word in the filter to find
	int uncensored_len = 0;
	Word *curr_word = NULL;
	char *currStartOfString = rawText; // A pointer that moves down the buffer
	bool beginOk = false; // this is true if the character before the match is a space or punctuation
	bool endOk = false; // this is true if the character after the match is a space or punctuation

	// go through the words
	for(curr_word = filter->head; curr_word != NULL; curr_word = curr_word->next) {
		uncensored = curr_word->uncensored;
		if(!uncensored) continue;

		currStartOfString = rawText; // reset the pointer to the start of the rawText
		uncensored_len = strlen(uncensored);

		//go through the line to replace all instances.
		while (1) {
		    match = strcasestr (currStartOfString, uncensored);
		    if (match == NULL) break;

		    // The match is the first word in the string so you don't need to check for index -1
		    if (match == currStartOfString) {
		      beginOk = true;
		    }
		    // If the index before the match is a space or is punctuation, then it is ok
		    else if (match > currStartOfString && match[-1] != '/' && (isspace (match[-1]) || ispunct(match[-1]))) {
		      beginOk = true;
		    }

		    // If the index after the match is a space or is punctuation, then it is ok
		    if (isspace(match [uncensored_len]) || match[uncensored_len] == '\0' || ispunct(match[uncensored_len]) ) {
		      endOk = true;
		    }
		    //but if the index after the match is a /, then it is baaad. 
			if(match[uncensored_len] == '/') endOk = false;

		    // Only filter if the word is valid (aka, it's not part of another word like... class, cassie, or widick
		    if (beginOk && endOk) {
		      int i;
		      for(i=0; i<uncensored_len; i++) {
			match[i] = '*'; // replace all characters with a star
		      }
		    }

		    beginOk = false; // reset boolean values for next strcasestr check
		    endOk = false;
		    currStartOfString = match + strlen(uncensored); // move the pointer that strstr will look through
		}
	}
}

/**
 * Replaces all bad words in the inFile and saves the results in outFile.
 * 
 * @param filter The filter to use
 * @param inFile The file to read
 * @param outFile The filtered version of the input file.
 */
void Filter_filterFile(Filter *filter, FILE* inFile, FILE* outFile)
{
	if(!filter || !inFile || !outFile) return;
	rewind(inFile);
	rewind(outFile);

	if(!Filter_isTextFile(filter, inFile)) {
		char ch;
		// Copy infile into outfile
		while( ( ch = fgetc(inFile) ) != EOF ) fputc(ch, outFile);
		return;
	}

	rewind (inFile);
	rewind (outFile);

	char linebuffer[FILTER_BUFFER_SIZE+2];
	// Read in one line from the buffer
	while(fgets(linebuffer, FILTER_BUFFER_SIZE, inFile)){
	  // line buffer may contain bad words
	  Filter_filterLine(filter, linebuffer);
	  // line buffer should have stars over bad words
	  fputs(linebuffer, outFile); // write the output to the outFile
	}
}
